﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OverStore.Configuration;
using OverStore.Configuration.Builder.Fluent;
using System.Data.SqlServerCe;
using OverStore.Runtime.Storage;
using OverStore.Configuration.Components;

namespace OverStore.TutorialAsTest.Tutorial3.Lesson7
{
    /// <summary>
    /// <para>
    /// This class describes how to work with object hierarchies arisen due mapping one-to-many 
    /// assosiations (on the 'many' side of that one).
    /// </para>
    /// <para>
    /// This lessons will use two simple classes Document and DocumentType referenced by 
    /// one-to-many relation. (ONE DocumentType is referenced by MANY Documents).
    /// </para>
    /// <para>
    /// Database structure is created by following SQL commands:
    /// <code>
    /// CREATE TABLE DocumentType
    /// (
    ///    Id int not null primary key identity (1,1),
    ///    Name nvarchar(100) not null
    /// );
    /// 
    /// CREATE TABLE Document
    /// (
    ///    Id int not null primary key identity (1,1),
    ///    DocumentType int not null,
    ///    Date datetime not null,
    ///    Amount numeric(18,2) not null
    /// );
    /// 
    /// CREATE TABLE DocumentDetails
    /// (
    ///     Id int not null primary key identity (1,1),
    ///     DocumentId int not null,
    ///     Comment nvarchar(100) not null
    /// )
    /// 
    /// ALTER TABLE Document ADD CONSTRAINT FK_DocumentType FOREIGN KEY (DocumentType)
    /// REFERENCES DocumentType(ID);
    /// 
    /// ALTER TABLE DocumentDetails ADD CONSTRAINT FK_DocumentDetails FOREIGN KEY (DocumentId)
    /// REFERENCES Document(Id);
    /// 
    /// INSERT INTO DocumentType(Name) VALUES ('Expence');
    /// INSERT INTO DocumentType(Name) VALUES  ('Movement');
    /// 
    /// INSERT INTO Document(DocumentType, Date, Amount) VALUES (1, GETDATE(), 22.3);
    /// INSERT INTO Document(DocumentType, Date, Amount) VALUES (2, GETDATE(), 12.3);
    /// INSERT INTO Document(DocumentType, Date, Amount) VALUES (1, GETDATE(), -2.3);
    /// 
    /// INSERT INTO DocumentDetails(DocumentId, Comment) VALUES (1, 'Comment 1');
    /// INSERT INTO DocumentDetails(DocumentId, Comment) VALUES (1, 'Comment 2');
    /// 
    /// INSERT INTO DocumentDetails(DocumentId, Comment) VALUES (2, 'some notes');
    /// </code>
    /// </para>
    /// </summary>
    [TestClass]
    public class Lesson7
    {
        class DocumentType
        {
            public int Id { get; set; }

            public string Name { get; set; }
        }

        class Document
        {
            public Document()
            {
                Details = new List<DocumentDetails>();
            }

            public int Id { get; set; }

            public DocumentType DocumentType { get; set; }

            public DateTime Date { get; set; }

            public decimal Amount { get; set; }

            public List<DocumentDetails> Details { get; set; }
        }

        class DocumentDetails
        {
            public int Id { get; set; }

            public int DocumentId { get; set; }

            public string Comment { get; set; }
        }

        /// <summary>
        /// Creates the configuration with manual mappings. 
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <returns></returns>
        public static OverStoreConfiguration CreateManualConfiguration(string connectionString)
        {
            return OverStoreFluent.Configure()
                 .LogToConsole()
                 .ConnectToDatabase<SqlCeConnection>(connectionString, ProcessOpenedReadersMode.ForciblyCloseReaders)
                 .CustomClassRepository<DocumentType, int>()
                    .DelegateDepot()
                        .CreateInstance(() => new DocumentType())
                        .StoreIdentifier((e, id) => e.Id = id)
                        .Incremental(-1, -1)
                    .EndDepot()
                    .MapUsingDelegate()
                        .GetObjectDataUsing((entiy, name, provider) =>
                            {
                                if (name == "Name")
                                    return entiy.Name;
                                if (name == "orig__Id" || name == "Id")
                                    return provider.GetIdentifier<DocumentType, int>(entiy);
                                return System.Reflection.Missing.Value;
                            })
                        .ApplyDataUsing((data, entity, provider) =>
                            {
                                entity.Name = data.GetString("Name");
                                if (data.ContainsName("new__Id"))
                                    provider.PutNewIdentifier(entity, (int)data.GetDecimal("new__Id"));
                            })
                    .CommandsFromTable(false,
                        Component.CreateTable("DocumentType")
                            .DbGeneratedKey<int>("Id")
                            .Column<string>("Name"))
                    .TrackChangesUsingDump(e => StringComparer.CurrentCulture.GetHashCode(e.Name))
                 .EndObjectRepository()
                 .CustomClassRepository<Document, int>()
                    .Hierarchy()
                        .CollectionAggregate<DocumentDetails>(d => d.Details, (e, d, p) => d.DocumentId = p.GetIdentifier<Document, int>(e))
                    .EndHierarchy()
                    .EagerLoading()
                    .DelegateDepot()
                        .CreateInstance(() => new Document())
                        .StoreIdentifier((e, id) => e.Id = id)
                        .Incremental(-1, -1)
                    .EndDepot()
                    .MapUsingDelegate()
                        .GetObjectDataUsing((entity, name, provider) =>
                            {
                                switch (name)
                                {
                                    case "DocumentType":
                                        return entity.DocumentType == null ? null : (object)provider.GetIdentifier<DocumentType, int>(entity.DocumentType);
                                    case "Date":
                                        return entity.Date;
                                    case "Amount":
                                        return entity.Amount;
                                    case "Id":
                                    case "orig__Id":
                                        return provider.GetIdentifier<Document, int>(entity);

                                }
                                return System.Reflection.Missing.Value;
                            })
                        .ApplyDataUsing((data, entity, provider) =>
                            {
                                entity.Amount = data.GetDecimal("Amount");
                                entity.Date = data.GetDateTime("Date");

                                entity.DocumentType = provider.GetInstance<DocumentType, int>(data.GetInt32("DocumentType"));
                                if (data.ContainsName("new__Id"))
                                {
                                    var id = data.GetDecimal("new__Id");
                                    provider.PutNewIdentifier(entity, (int)id);
                                }
                            })
                    .CommandsFromTable(false,
                        Component.CreateTable("Document")
                            .DbGeneratedKey<int>("Id")
                            .Column<int>("DocumentType")
                            .Column<DateTime>("Date")
                            .Column<Decimal>("Amount"))
                    .TrackChangesUsingDump(d =>
                        {
                            unchecked
                            {
                                return d.DocumentType.GetHashCode() +
                                       d.Date.GetHashCode() +
                                       d.Amount.GetHashCode();
                            }
                        })
                    .ByIdentifierAttaching(e => e.Id)
                .EndObjectRepository()
                .CustomClassRepository<DocumentDetails, int>()
                    .DelegateDepot()
                        .CreateInstance(() => new DocumentDetails())
                        .StoreIdentifier((e, id) => e.Id = id)
                        .Incremental(-1, -1)
                    .EndDepot()
                    .MapUsingDelegate()
                        .GetObjectDataUsing((entiy, name, provider) =>
                        {
                            if (name == "Comment")
                                return entiy.Comment;
                            if (name == "DocumentId")
                                return entiy.DocumentId;
                            if (name == "orig__Id" || name == "Id")
                                return provider.GetIdentifier<DocumentDetails, int>(entiy);
                            return System.Reflection.Missing.Value;
                        })
                        .ApplyDataUsing((data, entity, provider) =>
                        {
                            entity.Comment = data.GetString("Comment");
                            entity.DocumentId = data.GetInt32("DocumentId");
                            if (data.ContainsName("new__Id"))
                                provider.PutNewIdentifier(entity, (int)data.GetDecimal("new__Id"));
                        })
                    .CommandsFromTable(false,
                        Component.CreateTable("DocumentDetails")
                            .DbGeneratedKey<int>("Id")
                            .Column<int>("DocumentId")
                            .Column<string>("Comment"))
                    .TrackChangesUsingDump(e =>
                        {
                            unchecked
                            {
                                return StringComparer.CurrentCulture.GetHashCode(e.Comment) +
                                       e.DocumentId.GetHashCode();
                            }
                        })
                    .ByIdentifierAttaching(d => d.Id)
                    .ParameterizedReader<Document>(true)
                            .ReadCommand()
                                .UseSqlCommand("select * from DocumentDetails where DocumentId = @DocumentId")
                                .InputParameter("DocumentId", System.Data.DbType.Int32)
                            .EndCommand()
                            .DelegateMapper((doc, name, p) =>
                            {
                                if (name == "DocumentId")
                                    return p.GetIdentifier<Document, int>(doc);
                                return System.Reflection.Missing.Value;
                            })
                            .DelegateIdentifierProvider(d => d.GetInt32("Id"))
                        .EndReader()
                 .EndObjectRepository()
            .End();
        }

        /// <summary>
        /// Creates configuration using table mapping.
        /// </summary>
        public static OverStoreConfiguration CreateTableMappingConfiguration(string connectionString)
        {
            return OverStoreFluent.Configure()
                    .LogToConsole()
                    .ConnectToDatabase<SqlCeConnection>(connectionString, ProcessOpenedReadersMode.ForciblyCloseReaders)
                    .MapClassToTable<DocumentType, int>("DocumentType")
                        .MapDbGeneratedKey("Id", e => e.Id, (e, v) => e.Id = v, Component.IdentifierGenerator.Seq<DocumentType>(-1, -1))
                        .MapScalarValue<string>("Name", e => e.Name, (e, v) => e.Name = v)
                    .EndTableMapping()
                    .MapClassToTable<DocumentDetails, int>("DocumentDetails")
                        .MapDbGeneratedKey("Id", e => e.Id, (e, v) => e.Id = v, Component.IdentifierGenerator.Seq<DocumentDetails>(-1, -1))
                        .MapScalarValue<int>("DocumentId", e => e.DocumentId, (e, v) => e.DocumentId = v)
                        .MapScalarValue<string>("Comment", e => e.Comment, (e, v) => e.Comment = v)
                        .ParameterizedReader<Document>(true)
                            .ReadCommand()
                                .UseSqlCommand("select * from DocumentDetails where DocumentId = @DocumentId")
                                .InputParameter("DocumentId", System.Data.DbType.Int32)
                            .EndCommand()
                            .DelegateMapper((doc, name, p) =>
                                {
                                    if (name == "DocumentId")
                                        return p.GetIdentifier<Document, int>(doc);
                                    return System.Reflection.Missing.Value;
                                })
                            .DelegateIdentifierProvider(d => d.GetInt32("Id"))
                        .EndReader()
                    .EndTableMapping()
                    .MapClassToTable<Document, int>("Document")
                        .MapDbGeneratedKey("Id", e => e.Id, (e, v) => e.Id = v,
                            Component.IdentifierGenerator.Seq<Document>(-1, -1))
                        .MapReference<DocumentType, int>("DocumentType", d => d.DocumentType, (d, v) => d.DocumentType = v)
                        .MapScalarValue<DateTime>("Date", e => e.Date, (e, v) => e.Date = v)
                        .MapScalarValue<Decimal>("Amount", e => e.Amount, (e, v) => e.Amount = v)
                        .MapCollection<DocumentDetails>(d => d.Details, (doc, detail, p) => detail.DocumentId = p.GetIdentifier<Document, int>(doc), true)
                    .EndTableMapping()
               .End();
        }

        /// <summary>
        /// Tests the configuration is correct.
        /// </summary>
        private void TestConfigCorrect(Func<IPersistenceSession> createSession)
        {
            using (var session = createSession())
            {
                var docType = session.GetByIdentifier<DocumentType, int>(1);
                Assert.AreEqual(1, docType.Id);
                Assert.AreEqual(docType.Name, "Expence");

                var doc = session.GetByIdentifier<Document, int>(2);
                Assert.AreEqual(2, doc.Id);
                Assert.AreEqual(2, doc.DocumentType.Id);
                Assert.AreEqual(12.3M, doc.Amount);

                var newDoc = new Document()
                {
                    Amount = 14.5M,
                    Date = DateTime.Now,
                    DocumentType = session.GetByIdentifier<DocumentType, int>(2),
                };
                session.Add(newDoc);
                Assert.IsTrue(newDoc.Id < 0);
                session.Save(newDoc);
                Assert.IsTrue(newDoc.Id > 0);
            }
        }

        /// <summary>
        /// Checks that reference is not saved if instance is not changed.
        /// </summary>
        private void Reference_NotSavedIfUnchangedCore(Func<IPersistenceSession> createSession)
        {
            int docTypeId = 0;
            string origName = null;
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(1);
                docTypeId = doc.DocumentType.Id;
                origName = doc.DocumentType.Name;
                doc.DocumentType.Name = "Changed";
                session.Save(doc);
            }

            using (var session = createSession())
            {
                var docType = session.GetByIdentifier<DocumentType, int>(docTypeId);
                Assert.AreEqual(origName, docType.Name);
            }
        }

        /// <summary>
        /// Checks references is saved if both instance and reference are new.
        /// </summary>
        /// <param name="createSession">The create session.</param>
        private void Reference_InstanceAndReferenceBothNewCore(Func<IPersistenceSession> createSession)
        {
            int newDocId = 0;
            int newDocTypeId = 0;
            using (var session = createSession())
            {
                var doc = new Document
                {
                    Date = DateTime.Now,
                    Amount = 100.0M,
                    DocumentType = new DocumentType
                    {
                        Name = "Test"
                    }
                };
                session.Add(doc);
                Assert.AreSame(doc.DocumentType, session.GetByIdentifier<DocumentType, int>(doc.DocumentType.Id));
                session.Save(doc);
                newDocId = doc.Id;
                newDocTypeId = doc.DocumentType.Id;
            }
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(newDocId);
                Assert.AreEqual(100.0M, doc.Amount);
                Assert.AreEqual(newDocTypeId, doc.DocumentType.Id);
                Assert.AreEqual("Test", doc.DocumentType.Name);
            }
        }

        /// <summary>
        /// Checks reference is saved if instance is modified and reference is new. 
        /// </summary>
        private void Reference_InstanceModifiedReferenceNewCore(Func<IPersistenceSession> createSession)
        {
            int newDocTypeId = 0;
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(1);
                doc.DocumentType = new DocumentType
                    {
                        Name = "Test"
                    };
                session.Add(doc);
                Assert.AreSame(doc.DocumentType, session.GetByIdentifier<DocumentType, int>(doc.DocumentType.Id));
                session.Save(doc);
                newDocTypeId = doc.DocumentType.Id;
            }
            using (var session = createSession())
            {
                var docType = session.GetByIdentifier<DocumentType, int>(newDocTypeId);
                Assert.AreEqual("Test", docType.Name);
            }
        }

        /// <summary>
        /// Checks aggregate collection contains new element after reloading, if element is 
        /// saved to database outside collection.
        /// </summary>
        private void Assosiation_CollectionReloadedCorrectCore(Func<IPersistenceSession> createSession)
        {
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(2);
                Assert.AreEqual(1, doc.Details.Count);

                var details = new DocumentDetails
                {
                    DocumentId = 2,
                    Comment = "newitem"
                };
                session.Save(details);

                Assert.AreEqual(1, doc.Details.Count);

                session.Reload(doc);

                Assert.AreEqual(2, doc.Details.Count);
                Assert.IsTrue(doc.Details.Any(d => ReferenceEquals(d, details)));
            }
        }

        /// <summary>
        /// Checks items removed from collection are saved correctly.
        /// </summary>
        private void Assosiation_RemovedCollectionItemSavedCorrectlyCore(Func<IPersistenceSession> createSession)
        {
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(2);
                Assert.AreEqual(1, doc.Details.Count);
                doc.Details.Clear();
                session.Save(doc);
            }
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(2);
                Assert.AreEqual(0, doc.Details.Count);
            }
        }

        private void Assosiation_BothItemAndAggregateAreNewCore(Func<IPersistenceSession> createSession)
        {
            var docId = 0;
            using (var session = createSession())
            {
                var doc = new Document
                {
                    Amount = 111.0M,
                    Date = DateTime.Now,
                    DocumentType = session.GetByIdentifier<DocumentType, int>(1),
                    Details = new List<DocumentDetails>
                    {
                        new DocumentDetails { Comment = "Cool" },
                        new DocumentDetails { Comment = "New Comment" },
                        new DocumentDetails { Comment = "Some other comment" }
                    }
                };
                session.Save(doc);
                docId = doc.Id;
            }
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(docId);
                Assert.AreEqual(3, doc.Details.Count);
                Assert.IsTrue(doc.Details.Any(d => d.Comment == "Cool"));
                Assert.IsTrue(doc.Details.Any(d => d.Comment == "New Comment"));
                Assert.IsTrue(doc.Details.Any(d => d.Comment == "Some other comment"));
            }
        }

        private void Assosiation_RemoveExistingAndAddNewCore(Func<IPersistenceSession> createSession)
        {
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(2);
                Assert.AreEqual(1, doc.Details.Count);

                doc.Details.Clear();
                doc.Details.Add(new DocumentDetails { Comment = "coolstuff" });
                session.Save(doc);
            }
            using (var session = createSession())
            {
                var doc = session.GetByIdentifier<Document, int>(2);
                Assert.AreEqual(1, doc.Details.Count);
                Assert.AreEqual("coolstuff", doc.Details[0].Comment);
                Assert.AreEqual(doc.Id, doc.Details[0].DocumentId);
            }
        }

        [TestMethod]
        public void References_CheckConfig()
        {
            RunTestInTwoEnvironment(TestConfigCorrect);
        }

        [TestMethod]
        public void Reference_NotSavedIfInstanceUnchanged()
        {
            RunTestInTwoEnvironment(Reference_NotSavedIfUnchangedCore);
        }

        [TestMethod]
        public void Reference_InstanceAndReferenceBothNew()
        {
            RunTestInTwoEnvironment(Reference_InstanceAndReferenceBothNewCore);
        }

        [TestMethod]
        public void Reference_InstanceModifiedReferenceNew()
        {
            RunTestInTwoEnvironment(Reference_InstanceModifiedReferenceNewCore);
        }

        [TestMethod]
        public void Assosiation_CollectionIsReloadedCorrectly()
        {
            RunTestInTwoEnvironment(Assosiation_CollectionReloadedCorrectCore);
        }

        [TestMethod]
        public void Assosiation_RemovedCollectionItemSavedCorrectly()
        {
            RunTestInTwoEnvironment(Assosiation_RemovedCollectionItemSavedCorrectlyCore);
        }

        [TestMethod]
        public void Assosiation_BothItemAndAggregateAreNew()
        {
            RunTestInTwoEnvironment(Assosiation_BothItemAndAggregateAreNewCore);
        }

        [TestMethod]
        public void Assosiation_RemoveExistingAndAddNew()
        {
            RunTestInTwoEnvironment(Assosiation_RemoveExistingAndAddNewCore);
        }

        private void RunTestInTwoEnvironment(Action<Func<IPersistenceSession>> testMethodCore)
        {
            var cs = TestHelper.CreateTestDbCopyAndBuildConnectionString();
            testMethodCore(CreateManualConfiguration(cs).CreateSession);

            var cs2 = TestHelper.CreateTestDbCopyAndBuildConnectionString();
            testMethodCore(CreateTableMappingConfiguration(cs2).CreateSession);
        }
    }
}
